//
// Created by Gorun on 2022/5/3.
//

#ifndef LOXPP_INTERPRETER_H
#define LOXPP_INTERPRETER_H

#include <any>
#include <cmath>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>

#include "Error.h"
#include "Expr.h"
#include "Stmt.h"
#include "Token.h"
#include "RuntimeError.h"
#include "Environment.h"
#include "LoxCallable.h"
#include "LoxFunction.h"
#include "NativeFunctions.h"
#include "LoxReturn.h"
#include "LoxClass.h"
#include "LoxInstance.h"

class Interpreter: public ExprVisitor, public StmtVisitor{
public:
    shared_ptr<Environment> globals{new Environment};
private:
    shared_ptr<Environment> environment=globals;
    map<shared_ptr<Expr>, int> locals;

public:
    Interpreter() {
        globals->define("clock", make_shared<NativeClock>());
    }

    void interpret(const vector<shared_ptr<Stmt>>& statements) {
        try {
            for (const shared_ptr<Stmt>& statement: statements) {
                execute(statement);
            }
        } catch (RuntimeError& e) {
            runtimeError(e);
        }
    }

    void resolve(shared_ptr<Expr> expr, int depth) {
        locals[expr]=depth;
    }

private:
    //execute the statement
    any execute(const shared_ptr<Stmt>& stmt) {
        stmt->accept(*this);
        return {};
    }

    //evaluate the value of expression
    any evaluate(const shared_ptr<Expr>& expression) {
        return expression->accept(*this);
    }

public:
    //execute the statements(block)
    any executeBlock(const vector<shared_ptr<Stmt>>& statements,
                     shared_ptr<Environment> env) {
        shared_ptr<Environment> previous = this->environment;
        try {
            this->environment = std::move(env);

            for (const shared_ptr<Stmt>& statement: statements) {
                execute(statement);
            }
        } catch (...) { //There's no "finally for catch in C++, but we can use ...!
            this->environment = previous;
            throw;
        }

        this->environment = previous;
        return {};
    }

    //Functions for visiting Statement:
    //A statement will not produce a value, so any is equal to nullptr;
    any visitBlockStmt(shared_ptr<Block> stmt) override {
        executeBlock(stmt->statements, make_shared<Environment>(environment));
        return {};
    }

    any visitPrintStmt(shared_ptr<Print> stmt) override {
        any value = evaluate(stmt->expression);
        cout<<stringify(value);
        if (stmt->ln)
            cout<<'\n';
        return {};
    }

    any visitInputStmt(shared_ptr<Input> stmt) override {
        Token variable = stmt->variable;
        string value;
        getline(cin, value);
        //using existing assign expression to handle binding
        //environment->assign(stmt->variable, move(value));
        visitAssignExpr(
                make_shared<Assign>(variable, make_shared<Literal>(value))
                        );
        return {};
    }

    any visitExpressionStmt(shared_ptr<Expression> stmt) override {
        evaluate(stmt->expression);
        return {};
    }

    any visitVarStmt(shared_ptr<Var> stmt) override {
        any value=nullptr;
        if (stmt->initializer!=nullptr) {
            value = evaluate(stmt->initializer);
        }

        environment->define(stmt->name.lexeme, move(value));
        return {};
    }

    any visitFunctionStmt(shared_ptr<Function> stmt) override {
        auto function = make_shared<LoxFunction>(stmt, environment, false, false);
        environment->define(stmt->name.lexeme, function);
        return {};
    }

    any visitIfStmt(shared_ptr<If> stmt) override {
        if (isTruthy(evaluate(stmt->condition))) {
            execute(stmt->thenBranch);
        } else if (stmt->elseBranch!=nullptr) {
            execute(stmt->elseBranch);
        }
        return {};
    }

    any visitWhileStmt(shared_ptr<While> stmt) override {
        while (isTruthy(evaluate(stmt->condition))) {
            execute(stmt->body);
        }
        return {};
    }

    any visitReturnStmt(shared_ptr<Return> stmt) override {
        any value = nullptr;
        if (stmt->value!=nullptr)
            value = evaluate(stmt->value);
        throw LoxReturn{value};
    }

    any visitClassStmt(shared_ptr<Class> stmt) override {
        any superclass;
        if (stmt->superclass!=nullptr) {
            superclass = evaluate(stmt->superclass);
            if (superclass.type()!=typeid(shared_ptr<LoxClass>)) {
                throw RuntimeError(stmt->superclass->name, "Superclass must be a class.");
            }
        }

        environment->define(stmt->name.lexeme, nullptr);
        //shared_ptr<LoxClass> klass = make_shared<LoxClass>(stmt->name.lexeme);

        if (stmt->superclass!=nullptr) {
            environment = make_shared<Environment>(environment);
            environment->define("super", superclass);
        }

        map<string, shared_ptr<LoxFunction>> methods;
        for (shared_ptr<Function> method: stmt->methods) {
            auto function = make_shared<LoxFunction>(method, environment, false,
                                                     method->name.lexeme=="init");
            methods[method->name.lexeme]=function;
        }
        shared_ptr<LoxClass> superklass = nullptr;
        if (superclass.type()==typeid(shared_ptr<LoxClass>)) {
            superklass = any_cast<shared_ptr<LoxClass>>(superclass);
        }
        shared_ptr<LoxClass> klass = make_shared<LoxClass>(stmt->name.lexeme, superklass, methods);

        if (stmt->superclass!=nullptr) {
            environment=environment->enclosing;
        }

        environment->assign(stmt->name, klass);
        return {};
    }

    //Functions for visiting Expression:
    //A expression may produce four types of Lox Runtime Type expressions:
    //string, double, bool, nullptr
    any visitLiteralExpr(shared_ptr<Literal> expr) override {
        return expr->value;
    }

    any visitGroupingExpr(shared_ptr<Grouping> expr) override {
        return evaluate(expr->expression);
    }

    any visitVariableExpr(shared_ptr<Variable> expr) override {
        return lookUpVariable(expr->name, expr);
    }

    any visitUnaryExpr(shared_ptr<Unary> expr) override {
        any right = evaluate(expr->right);

        switch(expr->op.type) {
            case MINUS:
                checkNumberOperand(expr->op, right);
                return -any_cast<double>(right);
            case BANG:
                return !isTruthy(right);
            default:
                return {};
        }
    }

    any visitBinaryExpr(shared_ptr<Binary> expr) override {
        any left = evaluate(expr->left);
        any right = evaluate(expr->right);

        switch (expr->op.type) {
            case BANG_EQUAL:
                return !isEqual(left, right);
            case EQUAL_EQUAL:
                return isEqual(left, right);
            case GREATER:
                checkNumberOperands(expr->op, left, right);
                return any_cast<double>(left) > any_cast<double>(right);
            case GREATER_EQUAL:
                checkNumberOperands(expr->op, left, right);
                return any_cast<double>(left) >= any_cast<double>(right);
            case LESS:
                checkNumberOperands(expr->op, left, right);
                return any_cast<double>(left) < any_cast<double>(right);
            case LESS_EQUAL:
                checkNumberOperands(expr->op, left, right);
                return any_cast<double>(left) <= any_cast<double>(right);
            case MINUS:
                checkNumberOperands(expr->op, left, right);
                return any_cast<double>(left) - any_cast<double>(right);
            case PLUS:
                if (left.type()==typeid(double)&&right.type()==typeid(double)) {
                    return any_cast<double>(left) + any_cast<double>(right);
                } else if (left.type()==typeid(string)&&right.type()==typeid(string)) {
                    return any_cast<string>(left) + any_cast<string>(right);
                }
                throw RuntimeError(expr->op, "Operands must be numbers or two strings.");
            case SLASH:
                checkNumberOperands(expr->op, left, right);
                return any_cast<double>(left) / any_cast<double>(right);
            case STAR:
                checkNumberOperands(expr->op, left, right);
                return any_cast<double>(left) * any_cast<double>(right);
            default:
                return {};
        }
    }

    any visitTernaryExpr(shared_ptr<Ternary> expr) override {
        any condition = evaluate(expr->condition);
        if (isTruthy(condition)) {
            return evaluate(expr->thenBranch);
        } else {
            return evaluate(expr->elseBranch);
        }
    }

    any visitAssignExpr(shared_ptr<Assign> expr) override {
        any value = evaluate(expr->value);

        auto element = locals.find(expr);
        if (element!=locals.end()) {
            int distance = element->second;
            environment->assignAt(distance, expr->name, value);
        } else {
            globals->assign(expr->name, value);
        }
        return value;
    }

    any visitLogicalExpr(shared_ptr<Logical> expr) override {
        any left = evaluate(expr->left);

        if (expr->op.type == OR) {
            if (isTruthy(left)) return left;
        } else { //Else it must be AND
            if (!isTruthy(left)) return left;
        }

        return evaluate(expr->right);
    }

    any visitCallExpr(shared_ptr<Call> expr) override {
        any callee = evaluate(expr->callee);

        vector<any> arguments{};
        for (const auto& argument: expr->arguments) {
            arguments.push_back(evaluate(argument));
        }

        shared_ptr<LoxCallable> function;

        if (callee.type()==typeid(shared_ptr<LoxFunction>)) {
            function = any_cast<shared_ptr<LoxFunction>>(callee);
        } else if (callee.type()==typeid(shared_ptr<NativeClock>)) {
            function = any_cast<shared_ptr<NativeClock>>(callee);
        } else if (callee.type()==typeid(shared_ptr<LoxClass>)) {
            function = any_cast<shared_ptr<LoxClass>>(callee);
        } else {
            throw RuntimeError{expr->paren, "Can only call functions and classes."};
        }

        if (arguments.size()!=function->arity()) {
            throw RuntimeError{
                expr->paren, "Expected " +
                            to_string(function->arity()) + " arguments but got " +
                            to_string(arguments.size()) + "." };
        }

        return function->call(*this, move(arguments));
    }

    any visitGetExpr(shared_ptr<Get> expr) override {
        any object = evaluate(expr->object);
        if (object.type()==typeid(shared_ptr<LoxInstance>)) {
            return any_cast<shared_ptr<LoxInstance>>(object)->get(expr->name);
        }

        throw RuntimeError(expr->name, "Only instances have properties.");
    }

    any visitSetExpr(shared_ptr<Set> expr) override {
        any object = evaluate(expr->object);

        if (object.type()!= typeid(shared_ptr<LoxInstance>)) {
            throw RuntimeError(expr->name, "Only instances have fields.");
        }

        any value = evaluate(expr->value);
        any_cast<shared_ptr<LoxInstance>>(object)->set(expr->name, value);
        return {};
    }

    any visitThisExpr(shared_ptr<This> expr) override {
        return lookUpVariable(expr->keyword, expr);
    }

    any visitSuperExpr(shared_ptr<Super> expr) override {
        int distance = locals[expr];
        auto superclass = any_cast<shared_ptr<LoxClass>>(environment->getAt(distance, "super"));
        auto object = any_cast<shared_ptr<LoxInstance>>(environment->getAt(distance-1, "this"));
        shared_ptr<LoxFunction> method = superclass->findMethod(expr->method.lexeme);

        if (method==nullptr) {
            throw RuntimeError(expr->method, "Undefined property '"+expr->method.lexeme+"'.");
        }

        return method->bind(object);
    }

private:
    //Tool Functions below:
    static string stringify(const any& object) {
        //nil
        if (object.type()==typeid(nullptr)) return string{"nil"};
        //bool
        if (object.type()==typeid(bool))
            return any_cast<bool>(object)?string{"true"}:string{"false"};
        //number
        if (object.type()==typeid(double)) {
            double decimal = any_cast<double>(object);
            double integer = round(decimal);
            string text;
            if (abs(integer-decimal)<1e-9)
                text = to_string(static_cast<int>(integer));
            else
                text = to_string(decimal);
            return text;
        }
        //string
        if (object.type()==typeid(string)) {
            return any_cast<string>(object);
        }
        //LoxFunctions:
        if (object.type()==typeid(shared_ptr<LoxFunction>)) {
            return any_cast<shared_ptr<LoxFunction>>(object)->toString();
        }
        if (object.type()==typeid(shared_ptr<NativeClock>)) {
            return any_cast<shared_ptr<NativeClock>>(object)->toString();
        }
        //LoxClasses:
        if (object.type()==typeid(shared_ptr<LoxClass>)) {
            return any_cast<shared_ptr<LoxClass>>(object)->toString();
        }
        if (object.type()==typeid(shared_ptr<LoxInstance>)) {
            return any_cast<shared_ptr<LoxInstance>>(object)->toString();
        }
        //Nothing matched
        return string{"Error in stringify(): weird object type"};
    }

    static void checkNumberOperand(const Token& op, const any& operand) {
        if (operand.type() == typeid(double)) return;
        throw RuntimeError{op, "Operands must be numbers."};
    }

    static void checkNumberOperands(const Token& op,
                             const any& left, const any& right) {
        if (left.type()==typeid(double)
        && right.type()==typeid(double))
            return;
        throw RuntimeError{op, "Operands must be numbers."};
    }

    static bool isTruthy(const any& object) {
        if (object.type()==typeid(nullptr)) return false;
        if (object.type()==typeid(bool))
            return any_cast<bool>(object);
        return true;
    }

    static bool isEqual(const any& a, const any& b) {
        if (a.type()==typeid(nullptr)&&b.type()==typeid(nullptr)) return true;
        //Next will call a.operator==(), so a can't be nullptr
        if (a.type()==typeid(nullptr)) return false;

        //If a and b are in the same type
        if (a.type()==typeid(bool)&&b.type()==typeid(bool)) {
            return any_cast<bool>(a) == any_cast<bool>(b);
        } else if (a.type()==typeid(double)&&b.type()==typeid(double)) {
            return any_cast<double>(a) == any_cast<double>(b);
        } else if (a.type()==typeid(string)&&b.type()==typeid(string)) {
            return any_cast<string>(a) == any_cast<string>(b);
        }
        //And if not: it's always false
        return false;
    }

    any lookUpVariable(const Token& name, shared_ptr<Expr> expr) {
        auto element = locals.find(expr);
        if (element!=locals.end()) {
            int distance = element->second;
            return environment->getAt(distance, name.lexeme);
        } else {
            return globals->get(name);
        }
    }

};

#endif //LOXPP_INTERPRETER_H
